﻿//-----------------------------------------------------------------------
// <copyright file="DuplicateFinderTask.cs" company="Anthony Steele">
// This source forms part of the DuplicateFinder utility
// Hosted at http://www.codeplex.com/DuplicateFinder
//
// This source is available under the terms of the Microsoft Public License (Ms-PL)
// See http://duplicatefinder.codeplex.com/license
// All other rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace DuplicateFinder.Tasks
{
    using System;
    using System.Globalization;

    using DuplicateFinderLib;

    using Microsoft.Build.Framework;
    using Microsoft.Build.Utilities;

    /// <summary>
    /// The MSBuild task interface for the duplicate finder
    /// </summary>
    public class DuplicateFinder : Task
    {
        #region data

        // data from the build file, for the duplicate finder

        /// <summary>
        /// how many lines must be the same 
        /// before it is considered a duplicate
        /// </summary>
        private int duplicateThreshold;

        /// <summary>
        /// the number of non-whitespace chars 
        /// that first line must have 
        /// </summary>
        private int firstLineMinWidth;

        /// <summary>
        /// File to focus on
        /// </summary>
        private string focusFile;

        /// <summary>
        /// Ignore lines that start with these prefixes
        /// </summary>
        private string excludedLinePrefixes;

        // these control when the task decides to return "false" for error
        // the task will fail if
        // - LengthThresholdForError > 0 (0 is the default value)
        // - found CountThresholdForError or more duplicates 
        // of length LengthThresholdForError or more are found

        /// <summary>
        /// first count error threshhold
        /// </summary>
        private int countThresholdForError;

        /// <summary>
        /// First length error threshhold
        /// </summary>
        private int lengthThresholdForError;

        // set a second and third threshhold
        // e.g fail if more than 10 dups of length 10 or more, 
        // and fail if 1 or more dup of length 50 or more

        /// <summary>
        /// Second count error threshhold
        /// </summary>
        private int countThresholdForError2;

        /// <summary>
        /// Second length error threshhold
        /// </summary>
        private int lengthThresholdForError2;

        /// <summary>
        /// Third count error threshhold
        /// </summary>
        private int countThresholdForError3;
        
        /// <summary>
        /// third length error threshhold
        /// </summary>
        private int lengthThresholdForError3;

        /// <summary>
        /// the files that are scanned for duplicates
        /// </summary>
        private ITaskItem[] files;

        /// <summary>
        /// working var - count of dups that meet the first thresholds
        /// </summary>
        private int errorDuplicateCount;

        /// <summary>
        /// working var - count of dups that meet the second thresholds
        /// </summary>
        private int errorDuplicateCount2;

        /// <summary>
        /// working var - count of dups that meet the third thresholds
        /// </summary>
        private int errorDuplicateCount3;

        #endregion

        #region properties

        /// <summary>
        /// Gets or sets duplicate threshhold 
        /// set from build script
        /// </summary>
        public int DuplicateThreshold
        {
            get { return this.duplicateThreshold; }
            set { this.duplicateThreshold = value; }
        }

        /// <summary>
        /// Gets or sets focus file 
        /// set from build script
        /// </summary>
        public string FocusFile
        {
            get { return this.focusFile; }
            set { this.focusFile = value; }
        }

        /// <summary>
        /// Gets or sets first line min width 
        /// set from build script
        /// </summary>
        public int FirstLineMinWidth
        {
            get { return this.firstLineMinWidth; }
            set { this.firstLineMinWidth = value; }
        }

        /// <summary>
        /// Gets or sets the ExcludedLinePrefixes
        /// This can be multiple lines of text
        /// one prefix per line
        /// </summary>
        public string ExcludedLinePrefixes
        {
            get { return this.excludedLinePrefixes; }
            set { this.excludedLinePrefixes = value; }
        }

        /// <summary>
        /// Gets or sets files to inspect
        /// </summary>
        [Required]
        public ITaskItem[] Files
        {
            get { return this.files; }
            set { this.files = value; }
        }

        #endregion

        #region threshold properties

        /// <summary>
        /// Gets or sets threshhold of duplicate count before error 
        /// set from build script
        /// </summary>
        public int CountThresholdForError
        {
            get { return this.countThresholdForError; }
            set { this.countThresholdForError = value; }
        }

        /// <summary>
        /// Gets or sets threshhold of duplicate length before error 
        /// set from build script
        /// </summary>
        public int LengthThresholdForError
        {
            get { return this.lengthThresholdForError; }
            set { this.lengthThresholdForError = value; }
        }

        /// <summary>
        /// Gets or sets threshold 2 count
        /// </summary>
        public int CountThresholdForError2
        {
            get { return this.countThresholdForError2; }
            set { this.countThresholdForError2 = value; }
        }

        /// <summary>
        /// Gets or sets threshold 2 length
        /// </summary>
        public int LengthThresholdForError2
        {
            get { return this.lengthThresholdForError2; }
            set { this.lengthThresholdForError2 = value; }
        }

        /// <summary>
        /// Gets or sets threshold 3 count
        /// </summary>
        public int CountThresholdForError3
        {
            get { return this.countThresholdForError3; }
            set { this.countThresholdForError3 = value; }
        }

        /// <summary>
        /// Gets or sets threshold 3 length
        /// </summary>
        public int LengthThresholdForError3
        {
            get { return this.lengthThresholdForError3; }
            set { this.lengthThresholdForError3 = value; }
        }

        #endregion

        #region overrides

        /// <summary>
        /// Entry point for MSBuild
        /// </summary>
        /// <returns>true if the task suceeded, i.e. no threshold was exceeded</returns>
        public override bool Execute()
        {
            // set up
            this.ShowStartMessage();

            this.errorDuplicateCount = 0;
            FileDuplicateFinder fileDupFinder = new FileDuplicateFinder();
            fileDupFinder.OnDuplicate += this.FileDupFinder_OnDuplicate;
        
            // apply the parameters
            // apply the argument options
            if (this.DuplicateThreshold > 0)
            {
                fileDupFinder.DuplicateThreshold = this.DuplicateThreshold;
            }

            if (this.FirstLineMinWidth > 0)
            {
                fileDupFinder.FirstLineMinWidth = this.FirstLineMinWidth;
            }

            this.AddExcludedLinePrefixes(fileDupFinder);

            // import the files from the build script
            foreach (ITaskItem file in this.Files)
            {
                fileDupFinder.ReadFile(file.ItemSpec);
            }

            // find duplicates!
            if (!string.IsNullOrEmpty(this.FocusFile))
            {
                int fileCount = fileDupFinder.FindDuplicates(this.FocusFile);
                if (fileCount == 0)
                {
                    Log.LogMessage("No files matched " + this.FocusFile);
                }
            }
            else
            {
                fileDupFinder.FindDuplicates();
            }

            // report status
            if (fileDupFinder.ReadCount == 1)
            {
                Log.LogMessage("1 file read");
            }
            else
            {
                Log.LogMessage("{0} files read", fileDupFinder.ReadCount);
            }

            if (fileDupFinder.DuplicateCount == 1)
            {
                Log.LogMessage("1 duplicate found");
            }
            else
            {
                Log.LogMessage("{0} duplicates found", fileDupFinder.DuplicateCount);
            }

            bool result1 = this.CheckThreshold(this.errorDuplicateCount, this.LengthThresholdForError, this.CountThresholdForError);
            bool result2 = this.CheckThreshold(this.errorDuplicateCount2, this.LengthThresholdForError2, this.CountThresholdForError2);
            bool result3 = this.CheckThreshold(this.errorDuplicateCount3, this.LengthThresholdForError3, this.CountThresholdForError3);
            
            // passed overall if all of them passed
            return result1 && result2 && result3;
        }

        /// <summary>
        /// Check the length against threshholds
        /// </summary>
        /// <param name="actualCount">the number of duplicates longer than the threshold</param>
        /// <param name="lengthThresh">the threshold length</param>
        /// <param name="countThresh">the count of duplicates longer than the threshold</param>
        /// <returns>true if the threshhold was not exceeded</returns>
        private bool CheckThreshold(int actualCount, int lengthThresh, int countThresh)
        {
            // check error thresholds
            if (lengthThresh > 0)
            {
                if (actualCount >= countThresh)
                {
                    string errorMessage = String.Format(
                        CultureInfo.CurrentCulture,
                        "FAILED: {0} Duplicates {1} or more lines long were found, error threshold was {2}",
                        actualCount, 
                        lengthThresh, 
                        countThresh);

                    Log.LogError(errorMessage);
                    return false;
                }
                else
                {
                    string message = String.Format(
                        CultureInfo.CurrentCulture,
                        "Passed: {0} Duplicates {1} or more lines long were found, error threshold was {2}",
                        actualCount, 
                        lengthThresh, 
                        countThresh);

                    Log.LogMessage(message);
                    return true;
                }
            }
            else
            {
                // debug Log.LogMessage("no threshold");
                return true;
            }
        }

        /// <summary>
        /// process the excluded line prefixes
        /// </summary>
        /// <param name="fileDupFinder">the duplicate finder engine</param>
        private void AddExcludedLinePrefixes(FileDuplicateFinder fileDupFinder)
        {
            if (!string.IsNullOrEmpty(this.ExcludedLinePrefixes))
            {
                // split the input text into multiple lines
                string[] prefixLines = this.ExcludedLinePrefixes.Split('\n');

                foreach (string line in prefixLines)
                {
                    fileDupFinder.AddIgnoredLinePrefix(line);
                }
            }
        }

        /// <summary>
        /// Show a message for the task starting
        /// </summary>
        private void ShowStartMessage()
        {
            string message = LibraryVersion.ProgramName + " " + 
                LibraryVersion.Version.ToString(CultureInfo.CurrentCulture) + " " + 
                LibraryVersion.Copyright;
            Log.LogMessage(message);
        }

        /// <summary>
        /// Fired when a duplicate is found
        /// </summary>
        /// <param name="sender">the event sender</param>
        /// <param name="e">the event arguments</param>
        private void FileDupFinder_OnDuplicate(object sender, DuplicateEventArgs e)
       {
           Log.LogMessage("Duplicate of length {0} at: ", e.Length);
            foreach (LineItem dupItem in e.Items)
            {
                int endLine = dupItem.LineNumber + e.Length - 1;

                Log.LogMessage(" Line {0}-{1} in {2}", dupItem.LineNumber, endLine, dupItem.FileName);
            }

            if ((this.LengthThresholdForError > 0) && (e.Length >= this.LengthThresholdForError))
            {
                // count it towards the error result
                this.errorDuplicateCount++;
            }

            if ((this.LengthThresholdForError2 > 0) && (e.Length >= this.LengthThresholdForError2))
            {
                // count it for threshold 2
                this.errorDuplicateCount2++;
            }

            if ((this.LengthThresholdForError3 > 0) && (e.Length >= this.LengthThresholdForError3))
            {
                // count it for threshold 3
                this.errorDuplicateCount3++;
            }
       }

        #endregion
    }
}
